/*
 * @Description:
 * @Author: huangmin
 * @Email: 812949176@qq.com
 * @Date: 2023-02-04 17:49:10
 * @LastEditors: huangmin
 * @LastEditTime: 2023-03-10 14:23:54
 * @FilePath: /virtual-list/src/component/VirtualList/index.jsx
 */
/* eslint-disable react-hooks/exhaustive-deps */
import React, {
  useState,
  useEffect,
  useMemo,
  useRef,
  useImperativeHandle,
} from "react";
import { Spin } from "antd";
import PropTypes from "prop-types";
import { useSize, useUpdateEffect, useThrottleFn, useThrottle } from "ahooks";
import initWaterFallByDp from "./dp";
import {
  binarySearch,
  compareHeight,
  calcListOffsetTop,
  comparisonCache,
} from "./utils";
import styles from "./index.less";

const VirtualList = React.memo(
  React.forwardRef((props, ref) => {
    const {
      data,
      colNum = 2,
      gutter = [24, 24],
      style,
      itemRender = () => {},
      virtual = true,
      loadingOptions = {
        height: 50,
        render: null,
      },
      bottomOptions = {
        height: 0,
        render: null,
      },
      overScanCount = 1,
      className = "",
      itemWidth,
      scrollDom = null,
      dpThreshold = 60,
      reachBottomThreshold = 0,
      dynamicProgramming = false,
    } = props;
    const listRef = useRef(null);
    const listHeightArr = useRef(Array.from(Array(colNum).keys(), () => 0)); //保存每列高度
    const defaultHeight = 200;
    const { height: loadingHeight, render: loadingRender } = loadingOptions;
    const { height: bottomHeight, render: bottomRender } = bottomOptions;
    const [waterFallList, setWaterFallList] = useState([]);
    const [loading, setLoading] = useState(false);
    const [isInitData, setIsInitData] = useState(false);
    const [scrollTop, setScrollTop] = useState(0);
    const [cacheData, setCacheData] = useState(data);
    const containerWidthFirstChange = useRef(false);

    // 监听容器宽度变化
    const containerSize = useSize(listRef);
    const containerWidth = containerSize?.width;
    //宽度变化列表重排列加节流
    const throttleContainerWidth = useThrottle(containerWidth, {
      wait: 300,
      leading: false,
    });

    const col = useMemo(() => {
      if (containerWidth && itemWidth) {
        const newCol = Math.floor(
          (containerWidth + gutter[0]) / (itemWidth + gutter[0])
        );
        return newCol;
      } else {
        return colNum;
      }
    }, [colNum, containerWidth, itemWidth]);

    /**
     * @description: 计算需要渲染的数据（当前可视区+缓冲区overScanCount）
     * @return {*}
     */
    const computeVisible = () => {
      const newVisibleList = Array.from(Array(col).keys(), () => []);
      const clientHeight = scrollDom
        ? scrollDom.clientHeight || 0
        : document.body.clientHeight || 0;
      const startTop = scrollTop - clientHeight * overScanCount;
      const endTop = scrollTop + clientHeight * (overScanCount + 1);
      waterFallList.forEach((arr, i) => {
        const startIndex = binarySearch(waterFallList[i], startTop);
        for (let index = startIndex; index < arr.length; index++) {
          const { top, height, bottomHeight } = arr[index];
          if (top + height + bottomHeight >= startTop && top <= endTop) {
            newVisibleList[i] && newVisibleList[i].push(arr[index]);
          } else if (top + height + bottomHeight < startTop) {
            continue;
          } else {
            break;
          }
        }
      });
      return newVisibleList;
    };
    /**
     * @description: 计算瀑布流列表（贪心算法）
     * @param {*} originData
     * @param {*} start
     * @param {*} itemWidth
     * @param {*} list
     * @return {*}
     */
    const waterfallByGreedyAlgorithm = (originData, start, itemWidth, list) => {
      const [gapX, gapY] = gutter;
      const defaultBottomHeight = 0; //item项底部固定高度
      for (let i = start; i < originData.length; i++) {
        let index, marginTop;
        if (i < col) {
          index = i;
          marginTop = 0;
        } else {
          index = compareHeight(listHeightArr.current);
          // index = i % col;
          marginTop = gapY;
        }
        originData[i].top = listHeightArr.current[index] + marginTop;
        originData[i].left = index * (itemWidth + gapX);
        originData[i].width = itemWidth;
        originData[i].height = originData[i].size
          ? itemWidth / originData[i].size
          : defaultHeight; // 图片高度
        originData[i].bottomHeight = originData[i].bottomHeight
          ? originData[i].bottomHeight
          : defaultBottomHeight;
        originData[i].itemHeight =
          originData[i].bottomHeight + originData[i].height;
        list[index].push(originData[i]);
        listHeightArr.current[index] += originData[i].itemHeight + marginTop;
      }
      return list;
    };

    /**
     * @description: 计算瀑布流列表（动态规划）
     * @param {*} originData
     * @param {*} start
     * @param {*} itemWidth
     * @param {*} list
     * @return {*}
     */
    const waterfallByDynamicProgramming = (
      originData,
      start,
      itemWidth,
      list
    ) => {
      const [gapX, gapY] = gutter;
      const defaultBottomHeight = 0; //item项底部默认固定高度
      const heightsArr = [];
      for (let i = start; i < originData.length; i++) {
        const marginTop = gapY;
        const height = originData[i].size
          ? itemWidth / originData[i].size
          : defaultHeight; // 图片高度
        const bottomHeight = originData[i].bottomHeight
          ? originData[i].bottomHeight
          : defaultBottomHeight;
        heightsArr.push(height + marginTop + bottomHeight);
      }
      // const begin = Date.now();
      const dp = initWaterFallByDp(heightsArr, listHeightArr.current);
      // console.log('time',Date.now()-begin);
      if (dp) {
        dp.forEach((indexes, colIdx) => {
          indexes.forEach((idx) => {
            const i = idx + start;
            let marginTop = gapY;
            if (list[colIdx].length === 0) {
              marginTop = 0;
            }
            originData[i].top = listHeightArr.current[colIdx] + marginTop;
            originData[i].left = colIdx * (itemWidth + gapX);
            originData[i].width = itemWidth;
            originData[i].height = originData[i].size
              ? itemWidth / originData[i].size
              : defaultHeight; // 图片高度
            originData[i].bottomHeight = originData[i].bottomHeight
              ? originData[i].bottomHeight
              : bottomHeight;
            originData[i].itemHeight =
              originData[i].bottomHeight + originData[i].height;
            list[colIdx].push(originData[i]);
            listHeightArr.current[colIdx] +=
              originData[i].itemHeight + marginTop;
          });
        });
      }
      return list;
    };
    /**
     * @description: 初始化瀑布流列表数据，计算每一项的宽高和位置
     * @param {*} originData 数据源
     * @param {*} cache 是否使用缓存，减少计算
     * @return {*}
     */
    const initList = (originData, cache = false) => {
      const [gapX = 0] = gutter;
      const containerWidth = listRef.current?.clientWidth;
      const itemWidth = props.itemWidth
        ? props.itemWidth
        : (containerWidth - (col - 1) * gapX) / col;
      let newList;
      setIsInitData(true);
      if (originData) {
        let cacheFlag = false; //是否插入到旧数据后面
        if (cache && comparisonCache(originData, cacheData)) {
          cacheFlag = true;
          newList = [...waterFallList];
        } else {
          newList = Array.from(Array(col).keys(), () => []);
          listHeightArr.current = Array.from(Array(col).keys(), () => 0);
        }
        const start = cacheFlag ? cacheData.length : 0;
        if (dynamicProgramming) {
          // dpThreshold：一次最多能使用dp算法计算的阙值,当容器宽度或列数改变时，全部元素重排列dp计算非常消耗性能，强制使用贪心算法,仅对最后dpThreshold项dp
          const dpFlag = originData.length - start <= dpThreshold;
          if (dpFlag) {
            newList = waterfallByDynamicProgramming(
              originData,
              start,
              itemWidth,
              newList
            );
          } else {
            newList = waterfallByGreedyAlgorithm(
              originData.slice(0, originData.length - dpThreshold),
              start,
              itemWidth,
              newList
            );
            newList = waterfallByDynamicProgramming(
              originData,
              originData.length - dpThreshold,
              itemWidth,
              newList
            );
          }
        } else {
          newList = waterfallByGreedyAlgorithm(
            originData,
            start,
            itemWidth,
            newList
          );
        }
        setWaterFallList(newList);
        setIsInitData(false);
        setCacheData(originData);
      }
    };
    /**
     * @description: 滚动触底事件，500m节流
     * @return {*}
     */
    const { run: onReachBottomFn } = useThrottleFn(
      () => {
        props.onReachBottom && props.onReachBottom();
      },
      {
        wait: 500,
        leading: true,
        trailing: false,
      }
    );
    /**
     * @description: 监听滚动事件 100ms节流，当前列表相对滚动高度 = 容器滚动高度 - 列表距离容器顶部距离，
     * 当滚动到最小高度一列底部（默认）或距离底部reachBottomThreshold距离时触发触底事件
     * @return {*}
     */
    const { run: handleScroll } = useThrottleFn(
      (event) => {
        // 滚动的高度
        const target =
          scrollDom === null ? event.target.documentElement : event.target;
        const scrollTop = target ? target.scrollTop : 0;
        const listOffsetTop = calcListOffsetTop(listRef.current, scrollDom);
        const clientHeight = target ? target.clientHeight : 0;
        if (
          scrollTop + clientHeight - listOffsetTop >=
          (reachBottomThreshold
            ? Math.max(...listHeightArr.current) - reachBottomThreshold
            : Math.min(...listHeightArr.current))
        ) {
          onReachBottomFn();
        }
        if (virtual) {
          setScrollTop(scrollTop - listOffsetTop);
        }
      },
      {
        wait: 100,
        leading: false,
      }
    );
    /**
     * @description: 向父组件暴露方法，滚动到传入的id那一项，平滑滚动smooth未兼容safari
     * @return {*}
     */
    useImperativeHandle(ref, () => ({
      scrollTo(id) {
        const listOffsetTop = calcListOffsetTop(listRef.current, scrollDom);
        const item = waterFallList.flat(1).find((item) => item.id === id);
        item &&
          (scrollDom || window).scrollTo({
            top: item.top + listOffsetTop,
            behavior: "smooth",
          });
      },
    }));

    useEffect(() => {
      //首次挂载的初始化由throttleContainerWidth的变化触发
      if (!containerWidthFirstChange.current) {
        return;
      }
      initList(data, true);
    }, [data]);
    // 这里是因为有可能loading从true变化为false但下一页瀑布流数据还没计算渲染好导致高度塌陷，因此做一下延迟渲染。
    useEffect(() => {
      if (props.loading) {
        setLoading(props.loading);
      } else if (!isInitData) {
        setLoading(props.loading);
      }
    }, [props.loading, isInitData]);
    useEffect(() => {
      //开启页面滚动监听 节流防止重复触发
      const scrollElement = scrollDom || window;
      scrollElement.addEventListener("scroll", handleScroll);
      return () => {
        scrollElement.removeEventListener("scroll", handleScroll);
      };
    }, [scrollDom]);
    // 列数和列表项宽度变化时，都需要重新初始化计算瀑布流列表数据
    useUpdateEffect(() => {
      initList(data);
    }, [col, itemWidth]);
    // 容器宽度变化且未传itemWidth,则重新初始化计算瀑布流列表
    useEffect(() => {
      if (throttleContainerWidth && !itemWidth) {
        if (!containerWidthFirstChange.current) {
          containerWidthFirstChange.current = true;
        }
        initList(data);
      }
    }, [throttleContainerWidth]);

    const visibleList = useMemo(() => {
      const newVisibleList = virtual ? computeVisible() : waterFallList;
      return newVisibleList.flat(1);
    }, [waterFallList, scrollTop, virtual]);

    // absolute布局 脱离文档流不会导致页面频繁重排
    const listItemElement = (props) => {
      const { itemHeight, id, left, top, width } = props;
      return (
        <div
          className={styles.item}
          style={{
            height: itemHeight,
            transform: `translate(${left}px,${top}px)`,
            width,
          }}
          key={id}
        >
          {itemRender(props)}
        </div>
      );
    };

    return (
      <div
        ref={listRef}
        className={`${styles.waterFallListContainer} ${className}`}
        style={{
          height:
            Math.max(...listHeightArr.current) +
            (loading ? loadingHeight : 0) +
            bottomHeight,
          ...style,
        }}
      >
        {visibleList.map((item) => listItemElement(item))}
        <div className={styles.listBottom}>
          <div
            className={styles.bottomSlot}
            style={{
              height: bottomHeight,
            }}
          >
            {bottomRender}
          </div>
          <div
            className={styles.loading}
            style={{
              height: loadingHeight,
              display: loading ? "flex" : "none",
            }}
          >
            {loadingRender ? loadingRender : <Spin />}
          </div>
        </div>
      </div>
    );
  })
);
VirtualList.propTypes = {
  data: PropTypes.array, //列表数据 [{id,size}]
  className: PropTypes.string, //外部样式类
  gutter: PropTypes.array, //列表项水平和数值方向间隙 [x,y]
  loading: PropTypes.bool,
  loadingOptions: PropTypes.object, //列表loading样式 {height,render:ReactNode }
  bottomOptions: PropTypes.object, //列表底部插槽配置{height,render:ReactNode }
  reachBottomThreshold: PropTypes.number, //触发onReachBottom距离列表底部的阙值,默认为最小列高
  onReachBottom: PropTypes.func, //触底事件函数
  scrollDom: PropTypes.instanceOf(Element), //默认为window,滚动条处在的dom,需要设置css position属性
  style: PropTypes.object, //样式
  itemRender: PropTypes.func, //列表项渲染的节点函数,会将计算好的item宽高等属性作为参数传入({itemHeight,height,width,bottomHeight,...})=>{}
  itemWidth: PropTypes.number, // 列表项宽度，传入该属性时colNum失效
  virtual: PropTypes.bool, //是否开启虚拟列表优化
  overScanCount: PropTypes.number, //默认为1 在可视区域之外渲染几屏数据，值设置得越高，快速滚动时出现白屏的概率就越小，相应地，每次滚动的性能会变得越差
  dynamicProgramming: PropTypes.bool, //瀑布流是否开启动态规划算法 默认false 使用贪心算法
  dpThreshold: PropTypes.number, //dp算法阙值 默认60 开启动态规划模式时有效
};

export default VirtualList;
